package reports;

import gui.common.FileFormat;
import gui.common.ReportCallback;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import reports.builders.HTMLBuilder;
import reports.builders.IBuilder;
import reports.builders.PDFBuilder;
import reports.display.ReportTable;

import common.StringOps;

/**
 * A generic report, with a title and tables.
 * 
 * @author Ryan Cheatham
 * 
 *         {@invariant <tt>getTable() != null</tt>}
 */
public abstract class Report
{
  private String            title;
  private List<ReportTable> tables = new ArrayList<ReportTable>(2);
  private Exception         generationException;
  private boolean           success;

  /**
   * Constructs a new report with the given title.
   * 
   * @param title
   *          the new title
   * 
   *          {@pre <tt>title != null</tt>}
   * 
   *          {@pre <tt>title.length() > 0</tt>}
   * 
   *          {@post <tt>getTitle() == title</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the title is null or an empty string.
   */
  public Report(String title)
  {
    setTitle(title);
  }

  /**
   * Gets the title of this report.
   */
  public String getTitle()
  {
    return title;
  }

  /**
   * Sets the title of this report to the specified string.
   * 
   * @param title
   *          the new title
   * 
   *          {@pre <tt>title != null</tt>}
   * 
   *          {@pre <tt>title.length() > 0</tt>}
   * 
   *          {@post <tt>getTitle() == title</tt>}
   * 
   * @throws IllegalArgumentException
   *           if the title is null or an empty string.
   */
  public void setTitle(String title)
  {
    if (StringOps.isNullOrEmpty(title))
      throw new IllegalArgumentException("Invalid title - title is null or empty!");

    this.title = title;
  }

  /**
   * Adds the given table to this report.
   * 
   * @param table
   *          the table to add to this report
   * 
   *          {@pre table != null}
   * 
   * @throws NullPointerException
   *           if <tt>table</tt> is null.
   */
  protected void addTable(ReportTable table)
  {
    if (table == null)
      throw new NullPointerException();

    tables.add(table);
  }

  /**
   * Returns an iterator through all the tables in this report.
   * 
   * {@post <tt>retval != null</tt>}
   */
  public Iterator<ReportTable> getTables()
  {
    return Collections.unmodifiableList(tables).iterator();
  }

  /**
   * Builds this report as an HTML document, and returns it as a string.
   * 
   * @return an HTML representation of this report.
   */
  public File toHtml()
  {
    try
    {
      HTMLBuilder builder = new HTMLBuilder();
      buildReport(builder);
      String data = builder.getData();
      Path filePath = getTempFile(FileFormat.HTML);
      PrintWriter writer = new PrintWriter(new FileOutputStream(filePath.toFile()));
      writer.append(data);
      writer.close();
      return filePath.toFile();
    }
    catch (IOException e)
    {
      return null;
    }
  }

  /**
   * Builds this report as a PDF document, and returns it as raw byte data.
   * 
   * @return a PDF representation of this report.
   */
  public File toPdf()
  {
    PDFBuilder builder = new PDFBuilder();
    buildReport(builder);
    return builder.getData();
  }

  private Path getTempFile(FileFormat format) throws IOException
  {
    Path path = null;

    if (format.equals(FileFormat.HTML))
      path = Files.createTempFile(null, ".html");
    else if (format.equals(FileFormat.PDF))
      path = Files.createTempFile(null, ".pdf");

    return path;
  }

  protected abstract void buildReport(IBuilder<?> builder);

  /**
   * Generate the report. Because this may take a long time, it is done on a background thread.
   * Thus, it is very likely that the report will not be generated by the time this method returns.
   * If operations need to be performed on the report after it has been generated, or if a
   * notification needs to be sent after it has been generated, implement a {@link Runnable}
   * instance and pass it in as the callback.
   * 
   * @param callback
   *          a method to run once the report has been generated, or when an error occurs. Can be
   *          <tt>null</tt>.
   * 
   * @see Report#getGenerationException
   */
  public final void generateReport(ReportCallback callback)
  {
    try
    {
      visitData();

      interpretDataOnNewThread(callback);
    }
    catch (Exception e)
    {
      setGenerationException(e);
      setSuccess(false);
    }
  }

  private void interpretDataOnNewThread(final ReportCallback callback)
  {
    new Thread()
    {
      @Override
      public void run()
      {
        try
        {
          interpretData();

          setSuccess(true);
          callback.success();
        }
        catch (Exception e)
        {
          setGenerationException(e);
          setSuccess(false);
          callback.failure(e);
        }
        finally
        {
          callback.finished();
        }
      }
    }.start();
  }

  /**
   * <p>
   * Visit all the data here.
   * </p>
   * <p>
   * This should be where you iterate through the container tree and gather all the data you need
   * for the report.
   * </p>
   * 
   * @throws Exception
   *           an exception that occurs on generation failure. ONLY throw an exception if the
   *           report should be canceled.
   */
  protected abstract void visitData() throws Exception;

  /**
   * <p>
   * Work with the data here.
   * </p>
   * <p>
   * This should be the code that takes a while, like interpreting the data, building the table,
   * etc.
   * </p>
   * 
   * @throws Exception
   *           an exception that occurs on generation failure. ONLY throw an exception if the
   *           report should be canceled.
   */
  protected abstract void interpretData() throws Exception;

  /**
   * Sets the exception that occurred while generating the report.
   * 
   * @param e
   *          an exception that occurred while generating the report.
   */
  private void setGenerationException(Exception e)
  {
    generationException = e;
    System.err.println("Exception occurred while generating report: "
        + e.getClass().getSimpleName());
    System.err.println("Message: " + e.getMessage());
    e.printStackTrace();
  }

  /**
   * Returns the exception that occurred while generating a report if one occured, or <tt>null</tt>
   * otherwise.
   */
  public Exception getGenerationException()
  {
    return generationException;
  }

  /**
   * Sets whether the most recent report generation operation succeeded or not.
   * 
   * @param success
   *          whether the report was successfully generated or not.
   */
  protected void setSuccess(boolean success)
  {
    this.success = success;
  }

  /**
   * Returns whether the most recent attempt at generating this report succeeded or not.
   */
  public boolean success()
  {
    return success;
  }
}
